Swagger + Mariadb + Hibernate 实现极简CRUD
application.yaml
spring:
application:
name: Pluminary
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
username: root
password: root
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MariaDB103Dialect
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
server:
port: 8080
servlet:
context-path: /springboot
session:
timeout: 60
debug: true
com/pcy/Swagger/SwaggerConfig.java
package com.pcy.Swagger;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi createRestApi() {
return GroupedOpenApi.builder()
.group("Spring Boot 实战")
.pathsToMatch("/users/**") //这里是扫描包
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Boot 实战")
.version("1.0")
.description("Spring Boot 实战的 RESTFul 接口文档说明")
.contact(new Contact()
.name("Pluminary")
.url("https://github.com/P-luminary")
.email("390415030@qq.com")));
}
}
com/pcy/service/UserRepository.java //【这个是持久化接口 实现CRUD】
package com.pcy.service;
import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Integer> {
}
com/pcy/controller/UserController.java
package com.pcy.controller;
import com.pcy.dao.User;
import com.pcy.service.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {
@Autowired
private UserRepository userRepository;
@Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功获取用户信息"),
@ApiResponse(responseCode = "404", description = "未找到用户")
})
@GetMapping("/{id}")
public User get(@PathVariable int id) {
return userRepository.findById(id).orElse(null);
}
@Operation(summary = "创建用户", description = "创建一个新的用户")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功创建用户"),
@ApiResponse(responseCode = "400", description = "无效的输入")
})
@PostMapping
public User create(@RequestBody User user) {
return userRepository.save(user);
}
@Operation(summary = "更新用户", description = "更新用户信息")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功更新用户信息"),
@ApiResponse(responseCode = "404", description = "未找到用户")
})
@PutMapping
public User update(@RequestBody User user) {
return userRepository.save(user);
}
@Operation(summary = "删除用户", description = "根据ID删除用户")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功删除用户"),
@ApiResponse(responseCode = "404", description = "未找到用户")
})
@DeleteMapping("/{id}")
public void delete(@PathVariable int id) {
userRepository.deleteById(id);
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.pcy</groupId>
<artifactId>Pluminary</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Pluminary</name>
<description>Pluminary</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <version>8.0.33</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
增加分页、排序
com/pcy/controller/UserController.java
@Operation(summary = "获取用户列表", description = "获取用户列表")
@GetMapping
public Page<User> list(@RequestParam(defaultValue = "id") String property,
@RequestParam(defaultValue = "ASC")Sort.Direction direction,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
Pageable pageable = PageRequest.of(page, pageSize, direction, property);
return userRepository.findAll(pageable);
}
根据姓名查用户
com/pcy/controller/UserController.java
@Operation(summary = "根据姓名查用户",description = "根据姓名查用户")
@GetMapping("/name")
public List<User> getByName(String name){
return userRepository.findByNameContaining(name);
}
com/pcy/service/UserRepository.java
package com.pcy.service;
import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface UserRepository extends JpaRepository<User,Integer> {
List<User> findByNameContaining(String name);
}
根据生日查用户、删除User表
com/pcy/controller/UserController.java
@Operation(summary = "根据生日获取用户信息①",description = "根据生日获取用户信息①")
@GetMapping("/birthdayOne")
public List<User> getBirthDayOne(LocalDate birthDay){
return userRepository.findByBirthDay(birthDay);
}
@Operation(summary = "根据生日获取用户信息②",description = "根据生日获取用户信息②")
@GetMapping("/birthdayTwo")
public List<User> getBirthDayTwo(LocalDate birthDay){
return userRepository.findByBirthDayNative(birthDay);
}
@Operation(summary = "删除User",description = "删除User")
@GetMapping("/delete")
public void delete(){
userRepository.delete();
}
com/pcy/service/UserRepository.java
@Query("SELECT u FROM User u WHERE u.birthday=?1")
List<User> findByBirthDay(LocalDate birthDay);
@Query(value = "SELECT * FROM user WHERE birth_day =:birthDay",nativeQuery = true)
List<User> findByBirthDayNative(LocalDate birthDay);
@Modifying
@Transactional
@Query(value = "DELETE FROM User")
int delete();
增加审计
com/pcy/MallApplication.java //【增加@EnableJpaAuditing】
package com.pcy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
com/pcy/dao/BaseEntity.java //【没有必要为每个实体类都编写 直接封装导一个类 User去继承】
package com.pcy.dao;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Data
@MappedSuperclass
//该注解用于监听实体类,在save、update之后的状态
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedBy
@Column(updatable = false)
private String creator;
@LastModifiedBy
private String modifier;
@CreatedDate
@Column(updatable = false) //不可修改的
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
}
com/pcy/dao/User.java //【增加@EqualsAndHashCode 与 extends BaseEntity】
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
public class User extends BaseEntity{
@Id
// @Schema(description = "用户ID")
// @NotBlank(message = "Id不能为空")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
...
}
com/pcy/service/impl/AuditorAwareImpl.java
package com.pcy.service.impl;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 添加一个随机数
return Optional.of("管理员"+(int)(Math.random()));
}
}
引入Mybatis-Plus + FreeMarker
pom.xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
//根据你提供的实体类BaseEntity和User,我为你设计了一个基于MyBatis-Plus 3.5.x版本的代码生成器MysqlGenerator,它将自动生成与这些实体类相关的代码,如Mapper、Service、Controller等。以下是生成器的代码示例
【仅供查看学习 实际代码爆红无法导入】
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.builder.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Property;
import com.baomidou.mybatisplus.generator.keywords.MySqlKeyWordsHandler;
import java.util.Collections;
public class MysqlGenerator {
// 项目路径
private static final String PROJECT_PATH = System.getProperty("user.dir");
// 输出路径
private static final String OUTPUT_DIR = PROJECT_PATH + "/src/main/java";
// 作者
private static final String AUTHOR = "YourName";
// 包名
private static final String BASE_PACKAGE = "com.pcy";
// 数据源配置
private static final String DATABASE_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String DATABASE_USERNAME = "root";
private static final String DATABASE_PASSWORD = "password";
private static final String DATABASE_DRIVER = "com.mysql.cj.jdbc.Driver";
public static void main(String[] args) {
// 1. 全局配置
GlobalConfig.Builder globalConfig = new GlobalConfig.Builder()
.outputDir(OUTPUT_DIR)
.author(AUTHOR)
.enableSwagger()
.fileOverride()
.disableOpenDir(); // 不自动打开输出目录
// 2. 数据源配置
DataSourceConfig.Builder dataSourceConfig = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
.dbQuery(new MySqlQuery())
.schema("public")
.dbType(DbType.MYSQL)
.keyWordsHandler(new MySqlKeyWordsHandler())
.driverName(DATABASE_DRIVER);
// 3. 包配置
PackageConfig.Builder packageConfig = new PackageConfig.Builder()
.parent(BASE_PACKAGE)
.entity("dao")
.mapper("mapper")
.service("service")
.controller("controller");
// 4. 策略配置
StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder()
.addInclude("user") // 生成指定表
.addTablePrefix("t_") // 去掉表前缀
.entityBuilder()
.superClass(BaseEntity.class)
.enableLombok()
.addSuperEntityColumns("id", "creator", "modifier", "create_time", "update_time")
.logicDeleteColumnName("deleted")
.addTableFills(new Property("create_time", FieldFill.INSERT))
.addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE))
.enableActiveRecord()
.naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel)
.controllerBuilder()
.enableRestStyle()
.enableHyphenStyle()
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
.mapperBuilder()
.enableBaseResultMap()
.enableBaseColumnList();
// 5. 模板配置
TemplateConfig.Builder templateConfig = new TemplateConfig.Builder();
// 6. 自定义配置
InjectionConfig.Builder injectionConfig = new InjectionConfig.Builder()
.beforeOutputFile((tableInfo, objectMap) -> objectMap.put("parent", BASE_PACKAGE));
// 7. 整合配置
AutoGenerator autoGenerator = new AutoGenerator(dataSourceConfig.build())
.global(globalConfig.build())
.packageInfo(packageConfig.build())
.strategy(strategyConfig.build())
.template(templateConfig.build())
.injection(injectionConfig.build())
.templateEngine(new FreemarkerTemplateEngine()); // 选择模板引擎
// 8. 执行
autoGenerator.execute();
}
}
/*
关键配置说明:
GlobalConfig:设置代码生成的全局配置,包括作者、输出目录、是否覆盖已有文件等。
DataSourceConfig:配置数据库连接信息,使用MySQL数据库。
PackageConfig:指定生成的代码所在的包路径。
StrategyConfig:配置生成策略,包括实体类的继承关系、使用Lombok、Rest风格的控制器等。
TemplateConfig:模板配置,可定制生成的模板。
InjectionConfig:自定义配置,用于在生成文件前注入自定义的变量或逻辑。
AutoGenerator:整合所有配置并执行代码生成。
生成的文件包括:
实体类:根据数据库表生成实体类,并继承BaseEntity。
Mapper接口:生成Mapper接口用于数据库操作。
Service接口和实现类:生成Service接口及其实现类。
Controller类:生成Rest风格的控制器类。
使用方法:
修改数据库连接信息(DATABASE_URL、DATABASE_USERNAME、DATABASE_PASSWORD)。
配置需要生成代码的表名(addInclude("user"))。
运行MysqlGenerator.java的main方法,代码将会生成在指定的输出目录中。
*
//【以下都是自动生成的代码】
com/pcy/mapper/UserMapper.java
package com.pcy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
com/pcy/service/UserService.java
package com.pcy.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* 用户表 服务类
* </p>
*/
public interface UserService extends IService<User> {
// 在Spring中使用事务
@Transactional(propagation = Propagation.REQUIRED)
void addWithRequired(User user);
@Transactional(propagation = Propagation.REQUIRED)
void addWithRequiredAndException(User user);
@Transactional(propagation = Propagation.REQUIRES_NEW)
void addWithRequiredNew(User user);
@Transactional(propagation = Propagation.REQUIRES_NEW)
void addWithRequiredNewAndException(User user);
@Transactional(propagation = Propagation.NESTED)
void addWithNested(User user);
@Transactional(propagation = Propagation.NESTED)
void addWithNestedAndException(User user);
}
com/pcy/service/impl/UserServiceImpl.java
package com.pcy.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* 用户表 服务实现类
* </p>
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper mapper;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addWithRequired(User user) {
mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void addWithRequiredAndException(User user) {
mapper.insert(user);
throw new RuntimeException();
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addWithRequiredNew(User user) {
mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addWithRequiredNewAndException(User user) {
mapper.insert(user);
throw new RuntimeException();
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void addWithNested(User user) {
mapper.insert(user);
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void addWithNestedAndException(User user) {
mapper.insert(user);
throw new RuntimeException();
}
}
resources/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pcy.mapper.UserMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.pcy.entity.User">
<result column="id" property="id" />
<result column="creator" property="creator" />
<result column="modifier" property="modifier" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="name" property="name" />
<result column="email" property="email" />
<result column="birth_day" property="birthDay" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id,
creator,
modifier,
create_time,
update_time,
name, email, birth_day
</sql>
</mapper>
//【提问:爆bug "Could not autowire. No beans of 'UserMapper' type found"】 深度解析
1. @MapperScan 注解的原理 //启动类里面的 @MapperScan("com.pcy.mapper")
@MapperScan 是 MyBatis-Spring 提供的一个注解,用于指定要扫描的 Mapper 接口所在的包路径。它的作用是告诉 Spring 框架应该在哪些包路径下寻找 Mapper 接口,并将它们注册为 Spring 的 Bean。
扫描 Mapper 接口:Spring Boot 在启动时,会扫描你指定的包路径下的所有接口,并检测这些接口是否包含 MyBatis 的 Mapper 注解或者继承了 BaseMapper 等相关接口。
注册为 Bean:一旦找到这些接口,Spring 会自动为这些接口生成一个实现类,并将它们注册为 Spring 容器中的 Bean,这样你就可以通过 @Autowired 注入这些 Mapper。
2. @Mapper 注解的原理
@Mapper 是 MyBatis 提供的一个注解,用于标记一个接口为 MyBatis 的 Mapper 接口。被标记为 @Mapper 的接口会被 MyBatis-Spring 扫描到,并且 MyBatis 会为该接口生成一个实现类,负责执行 SQL 语句。
当你在 UserMapper 接口上添加 @Mapper 注解时,即使没有使用 @MapperScan,MyBatis 也会知道这个接口是一个 Mapper 接口,并将其注册为一个 Bean。这使得你可以在 UserServiceImpl 中通过 @Autowired 注入它。
3. 为什么使用 @MapperScan 和 @Mapper 不会报错
自动注册 Bean:@MapperScan 会自动扫描指定包路径下的所有 Mapper 接口,并将它们注册为 Spring 容器中的 Bean。这意味着在 UserServiceImpl 中,当你使用 @Autowired 注入 UserMapper 时,Spring 可以找到对应的 Bean,从而避免 Could not autowire 错误。
手动注册 Bean:当你在 Mapper 接口上直接使用 @Mapper 注解时,Spring 也会将该接口注册为一个 Bean,这样你同样可以通过 @Autowired 进行注入,而不会出现 Bean 找不到的问题。
//【提问:MysqlGenerator 逆向生成那些包的原理】
MyBatis-Plus 提供的 MyBatis-Plus Generator 是一个非常强大的代码生成工具,可以通过数据库表结构生成对应的 Java 代码,包括实体类、Mapper 接口、Mapper XML 文件、Service 类、Controller 类等。这个过程通常被称为“逆向工程”或“代码生成”。
1. MyBatis-Plus Generator 的工作原理
1.1 读取数据库表结构
数据源配置:首先,MyBatis-Plus Generator 通过配置的数据源连接到指定的数据库。它会读取数据库中的表结构信息,包括表名、字段名、数据类型、主键、外键、索引等信息。
> DataSourceConfig dsc = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
.driverName(DATABASE_DRIVER)
.build();
元数据解析:MyBatis-Plus Generator 通过 JDBC 获取数据库的元数据 (Metadata),并解析每个表的结构,将其转换为可以用于代码生成的数据结构。
1.2 生成代码
代码生成器:AutoGenerator 是核心的代码生成器类。它根据从数据库中获取的表结构信息,生成相应的 Java 类文件。
> AutoGenerator generator = new AutoGenerator(dsc);
模板引擎:MyBatis-Plus Generator 使用模板引擎(例如 Freemarker)来渲染代码模板。通过模板和解析后的元数据,生成代码文件。每个生成的 Java 类文件都对应着一个模板文件,模板文件中包含了如何生成特定类型文件的逻辑。
> generator.templateEngine(new FreemarkerTemplateEngine());
1.3 生成的包和文件
实体类 (entity):根据表结构生成对应的 Java 实体类。每个实体类与数据库表一一对应,包含表中字段的定义。
> strategyConfig.entityBuilder().enableLombok().naming(NamingStrategy.underline_to_camel);
Mapper 接口 (mapper):生成的 Mapper 接口用于与数据库交互,执行基本的增删改查操作。Mapper 接口通常继承自 BaseMapper,提供基本的 CRUD 操作。
> strategyConfig.mapperBuilder().enableBaseResultMap().enableBaseColumnList();
Mapper XML 文件 (mapper.xml):生成的 Mapper XML 文件包含了 Mapper 接口中对应的方法的 SQL 语句。这些 XML 文件用于定义复杂的查询、更新语句等。
Service 接口和实现类 (service, service.impl):Service 层是业务逻辑层。生成的 Service 接口提供了业务操作的定义,Service 实现类则实现这些业务操作。
> strategyConfig.serviceBuilder().formatServiceFileName("%sService");
Controller 类 (controller):生成的 Controller 类用于处理 HTTP 请求,调用 Service 层的方法进行业务处理,然后返回结果。Controller 通常与前端交互,处理用户请求。
> strategyConfig.controllerBuilder().enableRestStyle().enableHyphenStyle();
2. MyBatis-Plus Generator 如何生成这些包和文件
2.1 代码生成策略 (StrategyConfig)
StrategyConfig 类用于配置代码生成的策略,如生成哪些表,生成哪些类,类的命名规则,是否使用 Lombok 等。
StrategyConfig strategyConfig = new StrategyConfig.Builder()
.addInclude("user") // 生成指定表
.entityBuilder().enableLombok() // 实体类配置
.mapperBuilder().enableBaseResultMap() // Mapper 配置
.serviceBuilder().formatServiceFileName("%sService") // Service 配置
.controllerBuilder().enableRestStyle() // Controller 配置
.build();
2.2 模板文件
MyBatis-Plus Generator 使用的模板文件可以自定义,通常位于 resources/templates 目录下。每个模板文件对应一个需要生成的 Java 文件类型,例如 entity.java.ftl 对应实体类,mapper.java.ftl 对应 Mapper 接口。
模板文件中可以使用变量和逻辑来决定生成的代码内容。例如,${className} 会被替换为实际的类名,<#if useLombok> @Data </#if> 会根据条件生成代码。
2.3 文件输出配置 (InjectionConfig 和 FileOutConfig)
通过 InjectionConfig 和 FileOutConfig,可以控制生成文件的路径、名称、以及自定义生成的文件内容。例如,可以指定某个表的实体类生成到特定的包下,或者将 XML 文件输出到特定的路径。
InjectionConfig cfg = new InjectionConfig.Builder()
.beforeOutputFile((tableInfo, objectMap) -> {
// 自定义处理逻辑
})
.build();
用MyBatis Plus的分页
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.pcy</groupId>
<artifactId>Pluminary</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Pluminary</name>
<description>Pluminary</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <version>8.0.33</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<exclusion>
<artifactId>mybatis-spring</artifactId>
<groupId>org.mybatis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.5</version> <!-- 版本对齐 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
//【添加MyBatis-Plus的分页插件】
com/pcy/utils/MyBatisPlusConfig.java
package com.pcy.utils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration 用于定义配置类,被注解的类内部包含有一个或多个被@Bean注解的方法
// 用于构建bean定义,初始化Spring容器
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
return interceptor;
}
}
com/pcy/controller/UserController.java //【增加listPage】
package com.pcy.controller;
import com.pcy.entity.User;
import com.pcy.service.UserRepository;
import com.pcy.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
@Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
@GetMapping("/{id}")
public User get(@PathVariable int id) {
return userRepository.findById(id).orElse(null);
}
@Operation(summary = "创建用户", description = "创建一个新的用户")
@PostMapping
public User create(@RequestBody User user) {
return userRepository.save(user);
}
@Operation(summary = "更新用户", description = "更新用户信息")
@PutMapping
public User update(@RequestBody User user) {
return userRepository.save(user);
}
@Operation(summary = "删除用户", description = "根据ID删除用户")
@DeleteMapping("/{id}")
public void delete(@PathVariable int id) {
userRepository.deleteById(id);
}
@Operation(summary = "获取用户列表", description = "获取用户列表")
@GetMapping("/list")
public org.springframework.data.domain.Page<User> list(@RequestParam(defaultValue = "id") String property,
@RequestParam(defaultValue = "ASC") Sort.Direction direction,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
Pageable pageable = PageRequest.of(page, pageSize, direction, property);
return userRepository.findAll(pageable);
}
@Operation(summary = "根据生日获取用户信息①", description = "根据生日获取用户信息①")
@GetMapping("/birthdayOne")
public List<User> getBirthDayOne(@RequestParam LocalDate birthDay) {
return userRepository.findByBirthDay(birthDay);
}
@Operation(summary = "根据生日获取用户信息②", description = "根据生日获取用户信息②")
@GetMapping("/birthdayTwo")
public List<User> getBirthDayTwo(@RequestParam LocalDate birthDay) {
return userRepository.findByBirthDayNative(birthDay);
}
@Operation(summary = "删除所有用户", description = "删除所有用户")
@DeleteMapping("/deleteAll")
public void deleteAll() {
userRepository.deleteAll();
}
@Operation(summary = "分页查询用户列表", description = "分页查询用户列表")
@GetMapping("/page")
public Page<User> listPage(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
return userService.page(new Page<>(page, pageSize));
}
}
高级SQL语句(Lambda)
wrapper.lambda().like(user -> user.getName(), "p");
/*
Lambda 表达式:
user -> user.getName() 是一个 Lambda 表达式。
user 是 User 类的一个实例,作为 Lambda 表达式的输入参数。
user.getName() 是对 user 对象的 getName() 方法的调用,返回 name 字段的值。
作用:
这行代码告诉 MyBatis-Plus:在生成的 SQL 查询中,查找 name 字段值中包含 "p" 的所有记录。
wrapper.lambda() 返回一个 LambdaQueryWrapper<User> 对象,支持使用 Lambda 表达式进行条件构建。
.like() 方法添加了一个 LIKE 条件,表示在 SQL 查询中进行模糊匹配。
*/
wrapper.lambda().like(User::getName, "p");
/*
方法引用:
User::getName 是一种方法引用,它引用了 User 类的 getName() 方法。
方法引用是对 Lambda 表达式的一种简写。它表示将某个方法作为函数式接口的实现。
作用:
这行代码与第一行代码的作用相同,都是在生成的 SQL 查询中查找 name 字段值中包含 "p" 的所有记录。
User::getName 告诉 MyBatis-Plus:使用 User 类中的 getName() 方法来获取要参与条件判断的字段。
*/
com/pcy/controller/UserController.java
@Operation(summary = "自定义查询", description = "自定义查询")
@GetMapping("/Dingyi")
public List<User> getWrapper() { //类型List<User> 可以返回数据库列表
QueryWrapper<User> wrapper = new QueryWrapper<>();
// wrapper.eq("name", "潘春尧");
// wrapper.lambda().ge(User::getBirthDay, LocalDate.parse("2011-01-01"));
// wrapper.between(User::getBirthDay, "2011-01-01", "2011-12-31");
wrapper.lambda().like(User::getName, "string");
// wrapper.lambda().like(user -> user.getName(), "p");
// wrapper.select("name,count(*)").groupBy("name");
// return (QueryWrapper<User>) userMapper.selectList(wrapper);
// wrapper.in(CollectionUtils.isNotEmpty(nameList), User::getName, nameList);
return userMapper.selectList(wrapper);
}
自动填充、填充实现策略
com/pcy/utils/MyMetaObjectHandler.java
package com.pcy.utils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "creator", this::getCurrentUser, String.class);
this.strictInsertFill(metaObject, "modifier", this::getCurrentUser, String.class);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
this.strictUpdateFill(metaObject, "modifier", this::getCurrentUser, String.class);
}
// 模拟获取当前用户
private String getCurrentUser(){
return "管理员" + (int) (Math.random() * 10);
}
}
// 这是自动填充的原理
default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
if (metaObject.getValue(fieldName) == null) {
Object obj = fieldVal.get();
if (Objects.nonNull(obj)) {
metaObject.setValue(fieldName, obj);
}
}
return this;
}
强大的Druid
pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
// Druid和MariaDB是两种不同类型的数据库系统
1、类型和用途:
Druid:Druid是一种分布式的实时分析数据库,主要用于处理高吞吐量的时间序列数据或事件数据。它专为快速查询和分析大规模数据而设计,常用于数据仓库、在线分析处理(OLAP)以及实时数据分析等场景。
MariaDB:MariaDB是一种关系型数据库管理系统(RDBMS),它是MySQL的一个分支,广泛用于常规的事务处理、数据存储和管理。MariaDB通常用于传统的OLTP(在线事务处理)场景,如web应用、内容管理系统等。
2、适用场景:
Druid:适合用于实时数据分析、日志分析、时间序列分析、用户行为分析等需要快速响应的场景。
MariaDB:适合传统的数据库应用,如电子商务系统、内容管理系统、ERP、CRM等需要强事务处理能力的场景。
总结来说,Druid和MariaDB各自适用于不同的数据处理需求,Druid更侧重于实时分析和大规模数据处理,而MariaDB更侧重于事务处理和关系型数据管理
Spring Data JPA与MyBatis-Plus的区别并且简单举例说明
Spring Data JPA: //【实现接口】
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Other fields, getters, and setters
}
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
}
Spring Data JPA: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/jpa/users")
public class UserJpaController {
@Autowired
private UserRepository userRepository;
@GetMapping
public List<User> getAllUsers() {
return userRepository.findAll();
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
}
@PostMapping
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
user.setName(userDetails.getName());
// Update other fields here
return userRepository.save(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
userRepository.delete(user);
}
@GetMapping("/search")
public List<User> searchUsersByName(@RequestParam String name) {
return userRepository.findByName(name);
}
}
MyBatis Plus: //【实现接口】
@TableName("user")
public class User {
private Long id;
private String name;
// Other fields, getters, and setters
}
public interface UserMapper extends BaseMapper<User> {
// Custom SQL
@Select("SELECT * FROM user WHERE name = #{name}")
List<User> selectByName(@Param("name") String name);
}
MyBatis-Plus: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {
@Autowired
private UserMapper userMapper;
@GetMapping
public List<User> getAllUsers() {
return userMapper.selectList(null);
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userMapper.selectById(id);
}
@PostMapping
public void createUser(@RequestBody User user) {
userMapper.insert(user);
}
@PutMapping("/{id}")
public void updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User user = userMapper.selectById(id);
if (user == null) {
throw new ResourceNotFoundException("User not found with id: " + id);
}
user.setName(userDetails.getName());
// Update other fields here
userMapper.updateById(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new ResourceNotFoundException("User not found with id: " + id);
}
userMapper.deleteById(id);
}
@GetMapping("/search")
public List<User> searchUsersByName(@RequestParam String name) {
return userMapper.selectByName(name);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 构建查询条件的包装类,它使用 Lambda 表达式避免了手写字符串可能导致的字段错误。
// 这种方式非常适合需要根据多个条件动态生成SQL查询的场景,使用LambdaQueryWrapper不仅能提高代码的可读性,还能减少由于硬编码字符串导致的错误。
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {
@Autowired
private UserMapper userMapper;
@GetMapping("/search")
public List<User> searchUsersByName(@RequestParam String name) {
// 使用 LambdaQueryWrapper 构建模糊查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(User::getName, name); // 类似于 SQL 中的 "WHERE name LIKE '%name%'"
// 执行查询并返回结果
return userMapper.selectList(queryWrapper);
}
// 其他CRUD方法与前面的示例相同
}
Junit
经过单元测试,观察日志输出,就会发现没有进行数据库查询,对数据库的交互逻辑不是Service层的单元测试需要关心的事情,而是Dao层的单元测试需要考虑的。Service层的单元测试是假定Dao层全部正确的基础上写的,我们只需要关注Service层是正确即可。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
com/pcy/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper mapper;
public User getById(int id) {
logger.info("id为:",id);
return mapper.selectById(id);
}
......
}
这是测试Service
test/java com/pcy/service/impl/UserServiceImplTest.java //【用Mock改造 + log4j】
// 检查 UserServiceImpl 是否在测试中被 @MockBean 或其他方式替换为Mock对象。如果使用了Mock对象,测试时不会真正访问数据库,而是使用模拟数据。
package com.pcy.service.impl;
import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class UserServiceImplTest {
@InjectMocks
UserServiceImpl userService;
@Mock
UserMapper userMapper;
@Test
@DisplayName("Test Service getById")
void getById() {
// 模拟userMapper的selectById方法返回一个User对象
User mockUser = new User().setId(1).setName("qwe").setEmail("1234@qq.com");
Mockito.when(userMapper.selectById(1)).thenReturn(mockUser);
// 调用userService的getById方法,并验证返回结果
User user = userService.getById(1);
System.out.println(user);
Assertions.assertEquals("qwe", user.getName());
}
}
=====================================================================
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-12T21:18:07.805+08:00 INFO 31512 --- [Pluminary] [ main] com.pcy.service.impl.UserServiceImpl : id为:
User(id=1, name=qwe, age=0, email=1234@qq.com, birthDay=null)
com/pcy/entity/User.java
//你的 User 类同时使用了 Lombok 注解 (@Data, @Accessors(chain = true)) 和手动定义的 getter/setter 方法。由于 Lombok 已经生成了这些方法,手动定义的 getter/setter 方法会覆盖 Lombok 自动生成的方法,这可能导致链式调用的 setEmail 和其他类似方法无法正确解析。
package com.pcy.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDate;
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
@Accessors(chain = true) // 允许链式调用
public class User extends BaseEntity{
@Id
// @Schema(description = "用户ID")
// @NotBlank(message = "Id不能为空")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// @Schema(description = "用字")
// @NotBlank(message = "名字不能为空")
@Column(nullable = false, columnDefinition = "varchar(20) comment '姓名'")
private String name;
// @Transient //注解修饰
// @Schema(description = "年龄")
// @Min(value = 1, message = "年龄不能小于1")
private int age;
// @Schema(description = "邮箱")
// @Email(message = "E-mail格式不正确")
@Column(nullable = false, length = 50)
private String email;
// @Schema(description = "生日")
// @Past(message = "生日必须为过去的时间")
private LocalDate birthDay;
}
这是测试Controller
test/java com/pcy/controller/UserControllerTest.java
// Controller层的单元测试需要用到一个特定的类——MockMvc 专门为SpringMVC提供支持的
package com.pcy.controller;
import com.pcy.entity.User;
import com.pcy.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class UserControllerTest {
MockMvc mockMvc;
@Mock
UserService userService;
@InjectMocks
UserController userController;
@BeforeEach
void setUp(){
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
@Test
@DisplayName("Test Controller get")
void get() throws Exception {
Mockito.when(userService.getById(1)).thenReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
BDDMockito.given(userService.getById(1)).willReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)
.accept("application/json;charset=UTF-8")
.contentType("application/json;charset=UTF-8"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("刘水镜"))
.andDo(MockMvcResultHandlers.print())
.andReturn();
log.info("Test Controller get");
}
}
全局异常处理
/*
一、@RestControllerAdvice 注解的作用
@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。
@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。
@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。
因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。
*/
“全球”异常
com/pcy/controller/UserController.java
@Operation(summary = "异常查询", description = "异常查询")
@GetMapping(value = "/{id}")
public Result<User> get(@PathVariable Integer id) {
User user = userService.getById(id);
if (user == null){
throw new RuntimeException("找不到id信息" + id);
}
return Result.success(userService.getById(id));
}
/*
当输入id信息错误的时候
{
"code": 200,
"message": "操作成功",
"data": {
"creator": null,
"modifier": null,
"createTime": null,
"updateTime": null,
"id": 1,
"name": "潘春尧",
"age": 1,
"email": "390@qq.com",
"birthDay": "2024-08-10"
}
}
当输入id信息错误的时候
{
"code": 500,
"message": "找不到id信息3323",
"data": null
}
*/
com/pcy/utils/GlobalExceptionHandler.java
package com.pcy.utils;
import com.pcy.entity.MessageEnum;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//@ExceptionHandler注解用于在Spring MVC控制器中处理特定类型的异常。它可以应用于方法上
//当控制器方法抛出指定类型的异常时,@ExceptionHandler注解的方法将被调用来处理该异常
@ExceptionHandler(Exception.class)
public Result<Boolean> globalException(Exception e){
Result<Boolean> result = new Result<>();
result.setCode(MessageEnum.ERROR.getCode());
result.setMessage(e.getMessage() == null ? MessageEnum.ERROR.getMessage() : e.getMessage());
log.error(e.getMessage(), e);
return result;
}
}
com/pcy/entity/MessageEnum.java
package com.pcy.entity;
import lombok.Getter;
@Getter
public enum MessageEnum {
SUCCESS(200, "操作成功"),
ERROR(500, "操作失败");
private final Integer code;
private final String message;
MessageEnum(Integer code, String message){
this.code = code;
this.message = message;
}
}
com/pcy/entity/Result.java
package com.pcy.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
// 用于生成一个没有具体数据内容的成功响应
public static <T> Result<T> success(){
return success(null);
}
// 用于生成包含数据的成功响应
public static <T> Result<T> success(T data){
return new Result<>(MessageEnum.SUCCESS.getCode(), MessageEnum.SUCCESS.getMessage(), data);
}
// 用于生成一个没有具体错误信息的默认错误响应
public static<T> Result<T> error(){
return error(MessageEnum.ERROR);
}
// 用于生成带有特定错误信息的错误响应,MessageEnum 是一个枚举类型,包含了不同的错误信息和代码。
public static<T> Result<T> error(MessageEnum messageEnum){
return new Result<>(messageEnum.ERROR.getCode(), messageEnum.getMessage(), null);
}
// 用于生成包含自定义错误信息的错误响应
public static <T> Result<T> error(String message) {
return error(message, MessageEnum.ERROR.getCode());
}
// 用于生成包含自定义错误信息和自定义状态码的错误响应
protected static <T> Result<T> error(String message, Integer code) {
return new Result<>(code, message, null);
}
}
写个小异常
com/pcy/controller/ExceptionController.java
package com.pcy.controller;
import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/exception")
@Tag(name = "Exception", description = "异常操作")
public class ExceptionController {
@GetMapping("/runtimeexception")
public Result<Boolean> runtimeException(){
throw new RuntimeException();
}
}
/*
开启全局异常处理的返回值
{
"code": 500,
"message": "操作失败",
"data": null
}
没有全局异常处理的错误返回值
{
"timestamp": "2024-08-13T08:21:43.192+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/exception/runtimeexception"
}
*/
//在SwaggerConfig中添加扫描路径 "/exception/**" 不然接口无法获取
package com.pcy.Swagger;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi createRestApi() {
return GroupedOpenApi.builder()
.group("Spring Boot 实战")
.pathsToMatch("/users/**", "/exception/**")
// .addPathsToMatch("/exception/**")
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Boot 实战")
.version("1.0")
.description("Spring Boot 实战的 RESTFul 接口文档说明")
.contact(new Contact()
.name("Pluminary")
.url("https://github.com/P-luminary")
.email("390415030@qq.com")));
}
}
你提到的 GlobalExceptionHandler 和 ExceptionController 是用于统一处理 Spring MVC 控制器中的异常。让我逐步分析它们的作用,以及为什么在某些情况下它返回错误值。
//1. GlobalExceptionHandler 的作用
@RestControllerAdvice:这个注解用来全局处理控制器层的异常。它会拦截所有抛出的异常,并根据异常类型调用相应的 @ExceptionHandler 方法。
@ExceptionHandler(Exception.class):这个注解标注的方法会在控制器抛出 Exception 或其子类时执行。它用来捕获并处理全局的异常,比如你代码中的 RuntimeException。
globalException(Exception e):这是一个全局异常处理方法。当控制器中出现 Exception 时,这个方法会被调用。它将返回一个带有错误状态码的 Result<Boolean> 对象,并且会将错误信息记录到日志中。
//2. ExceptionController 的作用
@RestController:声明这个类是一个 Spring MVC 控制器,处理 Web 请求并返回数据。
runtimeException() 方法:在这个方法中,你手动抛出了一个 RuntimeException,这会触发 GlobalExceptionHandler 中的 globalException 方法,并返回一个包含错误信息的 Result<Boolean> 对象。
//3. 为什么只有引用 runtimeException() 才返回错误值
runtimeException() 方法直接抛出了一个 RuntimeException,因此会被 GlobalExceptionHandler 捕获并处理。这就是为什么在访问 /exception/runtimeexception 时,你会看到返回的是错误信息。
//4. 在 get() 方法中返回 200 状态码的原因
在 get() 方法中,如果你传入的 id 是无效的,返回的 Result<User> 仍然会是 Result.success(userService.getById(id)),即使 userService.getById(id) 返回的是 null。这种情况下,你的 Result.success(null) 仍然会返回状态码 200,因为 Result.success() 的设计是用于表示成功状态的,且你没有抛出任何异常。
//5. 如何让 get() 方法在出错时返回错误信息
你可以通过以下方法来确保在 get() 方法中传入无效的 id 时,抛出异常并触发全局异常处理器:
手动抛出异常:
java
复制代码
@GetMapping(value = "/{id}")
public Result<User> get(@PathVariable Integer id) {
User user = userService.getById(id);
if (user == null) {
throw new RuntimeException("User not found with id: " + id);
}
return Result.success(user);
}
在 userService.getById(id) 方法中抛出异常:如果你的业务逻辑要求在找不到用户时抛出异常,那么可以在 userService.getById(id) 方法中实现这个逻辑。
//6. 总结
GlobalExceptionHandler 用于捕获和处理全局异常。
当你手动抛出 RuntimeException 或其他异常时,它会捕获并返回带有错误信息的 Result。
在 get() 方法中,如果你想要在找不到用户时返回错误信息,需要手动抛出异常,这样才能触发 GlobalExceptionHandler。
日志级别
方法一:直接编写使用
// [配置方法:一种是直接在application.yml文件中配置、另一种是在外置logback-spring.xml文件中配置]
logging:
pattern:
console: "%d - %m%n"
方法二:引用外置xml文件
resources/pom.xml <引用外部的配置>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
resources/application.yaml
logging:
config: classpath:logback-spring.xml
<如果你有更多样的配置需求,就需要使用外置XML文件的配置方式>
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 日志文件存放路径-->
<property name="PATH" value="C:/Users/Pluminary/Desktop/log"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!-- 文件日志格式 -->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n"/>
<!-- 控制台输出配置-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--日志输出格式-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
${CONSOLE_LOG_PATTERN}
</pattern>
</layout>
</appender>
<!-- INFO 级别日志文件输出配置-->
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--按级别过滤日志,只输出 INFO 级别-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!--当天日志文件名-->
<File>${PATH}/info.log</File>
<!--按天分割日志文件-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--历史日志文件名规则-->
<fileNamePattern>${PATH}/info.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!--按大小分割同一天的日志-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!--日志输出格式-->
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>${FILE_LOG_PATTERN}</Pattern>
</layout>
</appender>
<!-- ERROR 级别日志文件输出配置-->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--按级别过滤日志,只输出 ERROR 及以上级别-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<!--当天日志文件名-->
<File>${PATH}/error.log</File>
<!--按天分割日志文件-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--历史日志文件名规则-->
<fileNamePattern>${PATH}/error.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!--按大小分割同一天的日志-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!--日志输出格式-->
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>${FILE_LOG_PATTERN}</Pattern>
</layout>
</appender>
<!--日志级别-->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
</configuration>
<
Logback 能够精确区分并输出特定日志级别的错误,是通过 Appender 配置中的 Filter 机制实现的。在你的 Logback 配置文件中,RollingFileAppender 使用了不同的 Filter 来确保只有指定级别的日志信息会被记录到特定的日志文件中。
工作原理
LevelFilter 和 ThresholdFilter:
LevelFilter: 这个过滤器允许你指定只接受特定日志级别的日志。例如,LevelFilter 被配置为只接受 INFO 级别的日志,而拒绝其他级别的日志。<level>INFO</level> 表示只记录 INFO 级别的日志。
ThresholdFilter: 这个过滤器允许你指定一个日志级别的下限,只有高于或等于这个级别的日志才会被记录。例如,ThresholdFilter 被配置为只接受 ERROR 级别及以上的日志(例如 ERROR 和 FATAL)。
日志级别的传递:
日志框架从最底层(比如 TRACE)开始逐级向上检查日志的级别,直到它与 Appender 中配置的 Filter 级别匹配。例如,如果一个 ERROR 级别的日志被触发,RollingFileAppender 的 ThresholdFilter 将检测到这个日志并允许它通过,然后将日志写入指定的 error.log 文件。
日志级别匹配:
当应用程序运行时,它会生成不同级别的日志信息(如 DEBUG、INFO、WARN、ERROR 等)。每个 Appender 都会根据它的 Filter 规则检查这些日志条目。只有符合条件的日志条目才会被记录到相应的日志文件中。
>
AOP切面
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
com/pcy/controller/AspectController.java
package com.pcy.controller;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
@GetMapping
public Result aspect(String message){
log.info("aspect controller");
return Result.success(message);
}
}
com/pcy/Swagger/SwaggerConfig.java //【增加"/aspect/**"】
package com.pcy.Swagger;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi createRestApi() {
return GroupedOpenApi.builder()
.group("Spring Boot 实战")
.pathsToMatch("/users/**", "/exception/**","/aspect/**")
// .addPathsToMatch("/exception/**")
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Boot 实战")
.version("1.0")
.description("Spring Boot 实战的 RESTFul 接口文档说明")
.contact(new Contact()
.name("Pluminary")
.url("https://github.com/P-luminary")
.email("390415030@qq.com")));
}
}
com/pcy/utils/WebAspect.java
package com.pcy.utils;
import com.pcy.entity.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class WebAspect {
// ★★★★★★★★★★★ 一定要注意这个AOP切面扫描的包 ★★★★★★★★★★★
@Pointcut("execution(public * com.pcy.controller.*.*(..))")
public void pointCut() {
}
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("======================================== 这是@Before ========================================");
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getName();
Object[] args = joinPoint.getArgs();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String, Object> paramMap = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
log.info("before path:{}",request.getServletPath());
log.info("before class name:{}",className);
log.info("before method name:{}",methodName);
log.info("before args:{}",paramMap.toString());
}
@After(value = "pointCut()")
public void after(JoinPoint joinPoint) {
System.out.println("======================================== 这是@After =========================================");
log.info("{} after", joinPoint.getSignature().getName());
}
@AfterReturning(value = "pointCut()", returning = "returnVal")
public void afterReturning(JoinPoint joinPoint, Object returnVal) {
System.out.println("==================================== 这是@AfterReturning ====================================");
log.info("{} after return, returnVal: {}", joinPoint.getSignature().getName(), returnVal);
}
}
/*
2024-08-14 18:28:20.249 INFO 3296 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-14 18:28:20.249 INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-08-14 18:28:20.250 INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2024-08-14 18:28:20.261 INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : preHandle开始时间:18:28:20:261 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:28:20.278 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : before path:/aspect
2024-08-14 18:28:20.278 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : before class name:com.pcy.controller.AspectController
2024-08-14 18:28:20.278 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : before method name:aspect
2024-08-14 18:28:20.278 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : before args:{message=www}
2024-08-14 18:28:20.278 INFO 3296 --- [nio-8080-exec-1] com.pcy.controller.AspectController : aspect controller
==================================== 这是@AfterReturning ====================================
2024-08-14 18:28:20.279 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : aspect after return, returnVal: Result(code=200, message=操作成功, data=www)
======================================== 这是@After =========================================
2024-08-14 18:28:20.280 INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect : aspect after
2024-08-14 18:28:20.308 INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : postHandle结束时间:18:28:20:308 毫秒
2024-08-14 18:28:20.308 INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : afterCompletion
2024-08-14 18:28:20.309 INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : 接口运行时间:47 毫秒
*/
若是调用UserController的get接口
com/pcy/controller/UserController.java
...
@Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
@GetMapping("/user/{id}")
public User get(@PathVariable int id) {
return userRepository.findById(id).orElse(null);
}
...
Console控制台的报错信息:
/*
2024-08-14 18:33:22.383 INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor : preHandle开始时间:18:33:22:383 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:33:22.385 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before path:/users/user/2
2024-08-14 18:33:22.385 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before class name:com.pcy.controller.UserController
2024-08-14 18:33:22.385 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before method name:get
2024-08-14 18:33:22.385 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before args:{id=2}
==================================== 这是@AfterReturning ====================================
2024-08-14 18:33:22.425 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : get after return, returnVal: User(id=2, name=we2, age=2, email=2, birthDay=2024-08-10)
======================================== 这是@After =========================================
2024-08-14 18:33:22.426 INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : get after
2024-08-14 18:33:22.428 INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor : postHandle结束时间:18:33:22:428 毫秒
2024-08-14 18:33:22.428 INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor : afterCompletion
2024-08-14 18:33:22.428 INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor : 接口运行时间:45 毫秒
*/
异常善后处理
com/pcy/controller/AspectController.java //【浏览exception接口的时候会报错】
package com.pcy.controller;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
@GetMapping
public Result aspect(String message){
log.info("aspect controller");
return Result.success(message);
}
@GetMapping("/exception")
public Result exception(){//抛出异常
throw new RuntimeException("runtime exception");
}
}
/*
======================================== 这是@Before ========================================
2024-08-15 15:09:20.586 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : before path:/aspect/exception
2024-08-15 15:09:20.586 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : before class name:com.pcy.controller.AspectController
2024-08-15 15:09:20.586 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : before method name:exception
2024-08-15 15:09:20.587 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : before args:{}
2024-08-15 15:09:20.587 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : exception after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:09:20.587 INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect : exception after
2024-08-15 15:09:20.588 ERROR 4200 --- [nio-8080-exec-4] com.pcy.utils.GlobalExceptionHandler : runtime exception
*/
com/pcy/utils/WebAspect.java
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
log.info("{} after throwing, message: {}", joinPoint.getSignature().getName(), e.getMessage());
}
综上所述:after方法不关心方法是否成功,当方法执行完成之后就会被执行;afterReturning方法必须在目标方法成果return之后才会被执行;afterThrowing方法则会在目标方法抛出异常后被执行
性能统计
Around可以囊括以上所有能力
com/pcy/controller/AspectController.java
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
@GetMapping("/sleep/{time}")
public Result sleep(@PathVariable("time") long time) {
log.info("sleep");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
log.error("error", e);
}
if (time == 1000) {
throw new RuntimeException("runtime exception");
}
log.info("wake up");
return Result.success("wake up");
}
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
log.info("around start");
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error("around error",e);
}
long endTime = System.currentTimeMillis();
log.info("execute time:{} ms",endTime - startTime);
return result;
}
//【当输入time值为2004时】
2024-08-15 15:27:21.987 INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor : preHandle开始时间:15:27:21:987 毫秒
2024-08-15 15:27:21.990 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : around start
======================================== 这是@Before ========================================
2024-08-15 15:27:21.991 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : before path:/aspect/sleep/2004
2024-08-15 15:27:21.991 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : before class name:com.pcy.controller.AspectController
2024-08-15 15:27:21.991 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : before method name:sleep
2024-08-15 15:27:21.991 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : before args:{time=2004}
2024-08-15 15:27:21.991 INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController : sleep
2024-08-15 15:27:23.996 INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController : wake up
==================================== 这是@AfterReturning ====================================
2024-08-15 15:27:23.997 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : sleep after return, returnVal: Result(code=200, message=操作成功, data=wake up)
======================================== 这是@After =========================================
2024-08-15 15:27:23.997 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : sleep after
2024-08-15 15:27:23.997 INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect : execute time:2007 ms
2024-08-15 15:27:23.999 INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor : postHandle结束时间:15:27:23:999 毫秒
2024-08-15 15:27:23.999 INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor : afterCompletion
2024-08-15 15:27:23.999 INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor : 接口运行时间:12 毫秒
//【当输入time值为1000时】
2024-08-15 15:28:19.596 INFO 10844 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor : preHandle开始时间:15:28:19:596 毫秒
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : around start
======================================== 这是@Before ========================================
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before path:/aspect/sleep/1000
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before class name:com.pcy.controller.AspectController
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before method name:sleep
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : before args:{time=1000}
2024-08-15 15:28:19.597 INFO 10844 --- [nio-8080-exec-9] com.pcy.controller.AspectController : sleep
2024-08-15 15:28:20.607 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : sleep after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:28:20.607 INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : sleep after
2024-08-15 15:28:20.607 ERROR 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect : around error
java.lang.RuntimeException: runtime exception
at com.pcy.controller.AspectController.sleep(AspectController.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
......
同一切面内的执行顺序
先执行before方法,再执行afterReturning / afterThrowing方法,最后执行after方法
要验证的关键点是around方法和它们之间的先后关系around方法早于before方法开始执行,并且晚于after方法结束执行,刚好将其他同志完全包裹了起来
//【注释掉WebAspect.java里面的代码不然会叠叠乐累加】
com/pcy/utils/AspectOne.java
package com.pcy.utils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectOne {
@Pointcut("execution(public * com.pcy.controller.*.*(..))")
public void pointCut(){}
@Before(value = "pointCut()")
public void before(){
log.info("before one");
}
@After(value = "pointCut()")
public void after(){
log.info("after one");
}
@AfterReturning(value = "pointCut()")
public void afterReturning(){
log.info("afterReturning one");
}
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
log.info("around one start");
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error("around error", e);
}
log.info("around one end");
return result;
}
}
/*
2024-08-15 16:12:12.819 INFO 28788 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-15 16:12:12.819 INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-08-15 16:12:12.820 INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2024-08-15 16:12:12.839 INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : preHandle开始时间:16:12:12:839 毫秒
2024-08-15 16:12:12.864 INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne : around one start
2024-08-15 16:12:12.864 INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne : before one
2024-08-15 16:12:12.864 INFO 28788 --- [nio-8080-exec-1] com.pcy.controller.AspectController : aspect controller
2024-08-15 16:12:12.865 INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne : afterReturning one
2024-08-15 16:12:12.865 INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne : after one
2024-08-15 16:12:12.865 INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne : around one end
2024-08-15 16:12:12.908 INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : postHandle结束时间:16:12:12:908 毫秒
2024-08-15 16:12:12.909 INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : afterCompletion
2024-08-15 16:12:12.909 INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor : 接口运行时间:69 毫秒
*/
不同切面间的执行顺序
将AspectOne复制两份命名AspectTwo和AspectThree [执行后是One→Three→Two]
在Spring中的加载顺序是根据类名升序排列的,Three字母排序排在Two前面
那如何指定执行顺序按照One Two Three?
分别为AspectOne/Two/Three加上@Order(1),@Order(2),@Order(3)
Redis
集成
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
application:
name: Pluminary
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
username: root
password: root
redis:
host: localhost port:6379
connect-timeout: 1000
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000
com/pcy/controller/HelloController.java
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/hello")
public String hello(){
stringRedisTemplate.opsForValue().set("hello","world");
return stringRedisTemplate.opsForValue().get("hello");
}
}
//先访问hello接口 再去redis-cli中尝试访问自己定义的内容
http://localhost:8080/swagger-ui/index.html#/hello-controller/hello
/*
127.0.0.1:6379> get hello
"world"
*/
Spring Security
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
com/pcy/controller/HelloController.java
package com.pcy.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
@GetMapping("/hi")
// http://localhost:8080/hi
public String hi(){
log.info("hi");
return "ok!";
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/hello")
public String hello(){
stringRedisTemplate.opsForValue().set("hello","world");
return stringRedisTemplate.opsForValue().get("hello");
}
}
/* Console:
Using generated security password: 4147707e-58d6-46d9-b5cc-19865a2c523f
*/
账号:user
密码:4147707e-58d6-46d9-b5cc-19865a2c523f
com/pcy/config/SecurityConfig.java
package com.pcy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 配置HTTP安全性
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
// .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 允许访问Swagger UI和API文档
.anyRequest().authenticated() // 所有请求都需要认证
)
.httpBasic(withDefaults()); // 使用HTTP Basic认证
return http.build();
}
// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.inMemoryAuthentication()
.withUser("pcy")
.password(passwordEncoder().encode("123456"))
.roles("admin");
return authenticationManagerBuilder.build();
}
// 配置密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
/*
你可能无法访问 http://localhost:8080/swagger-ui/index.html 的原因可能与 Spring Security 配置有关。由于你启用了 Spring Security,默认情况下,所有请求都需要经过身份认证,这可能会阻止你访问 Swagger UI。
为了确保你能够访问 Swagger UI,你需要在 Spring Security 的配置中添加一个例外规则,允许对 /swagger-ui/** 和相关的 Swagger 资源进行无认证访问。
添加代码:.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
账号:pcy
密码:123456
*/
从数据库中获取用户信息
com/pcy/config/SecurityConfig.java
package com.pcy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService) // 设置 UserDetailsService
.httpBasic(withDefaults())
.csrf(csrf -> csrf.disable());
return http.build();
}
// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsService) // 使用数据库中的用户信息
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
// 配置密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
com/pcy/service/impl/UserDetailsServiceImpl.java
package com.pcy.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pcy.entity.SysUser;
import com.pcy.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
//确保你的 UserDetailsServiceImpl 类被 Spring 管理,且实现了 Spring Security 的 UserDetailsService 接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
if (sysUser == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
return User.builder()
.username(sysUser.getUsername())
.password(sysUser.getPassword())
.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRole()))
.build();
}
}
com/pcy/entity/SysUser.java
package com.pcy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(name = "SysUser对象", description = "系统用户表")
public class SysUser extends Model<SysUser> {
private static final long serialVersionUID = 1L;
@Schema(description = "主键 id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "角色")
private String role;
@Override
public Serializable pkVal() {
return this.id;
}
}
com/pcy/service/SysUserService.java
package com.pcy.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;
public interface SysUserService extends IService<SysUser> {
String getCurrentUser();
}
com/pcy/service/impl/SysUserServiceImpl.java
package com.pcy.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.common.ApiException;
import com.pcy.entity.SysUser;
import com.pcy.mapper.SysUserMapper;
import com.pcy.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Override
public String getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 非匿名用户访问才能获得用户信息
if (!(authentication instanceof AnonymousAuthenticationToken)) {
String userName = authentication.getName();
log.info("userName by SecurityContextHolder: {}", userName);
return userName;
}
throw new ApiException("用户不存在!");
}
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;
/**
* <p>
* 系统用户表 Mapper 接口
* </p>
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
}
com/pcy/common/ApiException.java
package com.pcy.common;
import com.pcy.entity.MessageEnum;
import lombok.Data;
@Data
public class ApiException extends RuntimeException {
private Integer code;
public ApiException(MessageEnum messageEnum) {
super(messageEnum.getMessage());
this.code = messageEnum.getCode();
}
public ApiException(String message) {
super(message);
this.code = 500;
}
}
//【由于数据库的密码要被加密后的形式保存到数据中】
com/pcy/common/test.java
package com.pcy.common;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class test {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode("123456");
System.out.println(encodedPassword);
}
}
//$2a$10$GzDPdLyrzC9NudmE937AAetR2bef2VQzuSbP6KM6Y.I3045OuT/xC
修改创建SysUser用户的时候用Spring Security [登录的时候就可以用自己创建的了]
com/pcy/controller/UserController.java
/* 对比User数据
@Operation(summary = "创建User用户", description = "创建一个新的User用户")
@PostMapping("/create/")
public User create(@RequestBody User User) {
return userRepository.save(User);
}
*/
@Autowired
private SysUserService sysUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Operation(summary = "创建SysUser用户", description = "创建一个新的SysUser用户")
@PostMapping("/create/test")
public SysUser create(@RequestBody SysUser sysUser) {
sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
sysUserService.save(sysUser);
return sysUser;
}
package com.pcy.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;
public interface SysUserService extends IService<SysUser>{
String getCurrentUser();
}
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Override
public boolean save(SysUser sysUser) {
return SqlHelper.retBool(this.baseMapper.insert(sysUser));
}
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;
/**
* <p>
* 系统用户表 Mapper 接口
* </p>
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
}
权限控制
com/pcy/config/SecurityConfig.java
package com.pcy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/security/permitall").permitAll() // 允许所有人访问
.requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
.requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
.requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
.requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
.anyRequest().authenticated() // 其他所有请求需要认证
)
.userDetailsService(userDetailsService) // 设置 UserDetailsService
.httpBasic(withDefaults()) // 使用 HTTP Basic 认证
.csrf(csrf -> csrf.disable()); // 禁用 CSRF
return http.build();
}
// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder
.userDetailsService(userDetailsService) // 使用数据库中的用户信息
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
// 配置密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
com/pcy/controller/SecurityController.java
package com.pcy.controller;
import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/security")
@Tag(name = "权限控制", description = "权限控制")
public class SecurityController {
// Anyone
@Operation(summary = "permitAll 权限")
@GetMapping(value = "/permitall")
public Result<String> permitAll(){
return Result.success("permitAll");
}
// 未登录时可以访问
@Operation(summary = "anonymous 权限")
@GetMapping(value = "/anonymous")
public Result<String> anonymous(){
return Result.success("anonymous");
}
// xiaopan可以访问
@Operation(summary = "config 权限")
@GetMapping(value = "/config")
public Result<String> config(){
return Result.success("permitAll");
}
// xiaochun可以访问
@Operation(summary = "Secured 权限")
@GetMapping(value = "/Secured")
@Secured({"ROLE_Secured"})
public Result<String> Secured(){
return Result.success("Secured");
}
// panchunyao可以访问
@Operation(summary = "PreAuthorize 权限")
@GetMapping(value = "/preAuthorize")
@PreAuthorize("hasAnyAuthority('PreAuthorize')")
public Result<String> PreAuthorize(){
return Result.success("PreAuthorize");
}
}
/*
首先,确保在数据库中创建几个测试用户,并为每个用户分配不同的角色或权限。假设你有以下几个用户:
User 1: Username: xiaopan, Password: 123456, Role: ROLE_config
User 2: Username: xiaochun, Password: 123456, Role: ROLE_Secured
User 3: Username: panchun, Password: 123456, Authority: PreAuthorize
尝试使用不同用户登录:
使用 xiaopan 登录后,尝试访问 /security/config。
使用 xiaochun 登录后,尝试访问 /security/Secured。
使用 panchunyao 登录后,尝试访问 /security/preAuthorize。
检查响应:
/security/config: 只有 xiaopan 能访问,其他用户会被拒绝访问。
/security/Secured: 只有 xiaochun 能访问,其他用户会被拒绝访问。
/security/preAuthorize: 只有 panchunyao 能访问,其他用户会被拒绝访问。
/security/permitall: 所有用户都可以访问。
/security/anonymous: 只有未登录的用户可以访问,登录的用户会被拒绝。
验证权限控制
每个请求的响应应该反映你在 SecurityConfig 中配置的权限。
如果用户没有适当的角色或权限,应该会返回 403 Forbidden 或其他错误响应。
*/
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi createRestApi() {
return GroupedOpenApi.builder()
.group("Spring Boot 实战")
.pathsToMatch("/users/**", "/exception/**","/aspect/**","/test/**","/security/**")
// .addPathsToMatch("/exception/**")
.build();
}
}
// 【问答环节】
我的数据库创建的是Role字段但是为什么 下面这些有.hasAuthority 有.hasRole 还有其他的 这是怎么匹配到我数据库 按照你的方式设置的数据的
/*
1. hasRole() 和 hasAuthority() 的区别
hasRole(String role):
hasRole 方法通常用于检查用户是否拥有特定的角色。
Spring Security 会在你传递的角色名称前自动加上 "ROLE_" 前缀。因此,当你使用 hasRole("Secured") 时,实际上它会检查用户是否有 "ROLE_Secured" 这个权限。
hasAuthority(String authority):
hasAuthority 方法用于检查用户是否拥有特定的权限(或授权)。
hasAuthority 不会自动添加任何前缀。所以当你使用 hasAuthority("ROLE_config") 时,它会直接匹配 "ROLE_config",而不会添加任何前缀。
2. 匹配数据库中的角色和权限
hasRole("Secured"):
代码中的 hasRole("Secured") 实际上会匹配数据库中的 ROLE_Secured,因为 hasRole 方法会自动加上 "ROLE_" 前缀。
hasAuthority("ROLE_config"):
代码中的 hasAuthority("ROLE_config") 会直接匹配数据库中的 "ROLE_config",没有任何前缀变化。
hasAuthority("PreAuthorize"):
代码中的 hasAuthority("PreAuthorize") 会直接匹配数据库中的 "PreAuthorize",因为没有添加任何前缀。
*/
它为什么能查到我数据库的role字段里面的数据 如果我把这个字段换成test这个名字 它又是怎么去匹配到的呢
/*
Spring Security 默认会使用 UserDetails 接口中的 getAuthorities() 方法来获取用户的权限或角色信息。这些权限或角色信息通常是通过你在 UserDetailsService 实现类中定义的逻辑从数据库中获取的。
而在SecurityConfig中有代码:
@Autowired // 使用 Spring Security 的UserDetailsService
private UserDetailsService userDetailsService;
回顾securityFilterChain代码
下面会有 .userDetailsService(userDetailsService) // 设置 UserDetailsService
Spring Security 本身并不直接访问你的数据库表或字段。它依赖于你在 UserDetailsService 中提供的 UserDetails 对象的 getAuthorities() 方法的返回值。因此,当你在 SecurityConfig 中使用 hasRole() 或 hasAuthority() 方法时,它实际上是在检查用户的权限信息,即 UserDetails 对象中的 authorities。
===========================================================================
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
if (sysUser == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
return User.builder()
.username(sysUser.getUsername())
.password(sysUser.getPassword())
.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getTest())) // 修改为使用 'test' 字段
.build();
}
*/
记住我 √ Remember Me
基于SpringSession的方式
pom.xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
//【要新搞个登录界面 .ftl】
application.yaml
spring:
freemarker:
template-loader-path: /templates/
suffix: .ftl
resources/templates/loginPage.ftl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<input type="checkbox" id="remember-me" name="remember-me">
<label for="remember-me">Remember me</label>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>
com/pcy/controller/LoginController.java
package com.pcy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login") // 修改为 "/custom-login"
public String login() {
return "loginPage"; // 返回的视图名仍然是 "loginPage"
}
}
/*
http://localhost:8080/login
Please sign in
Username
panchunyao
Password
•••••••••••••
√ Remember me on this computer.
127.0.0.1:6379> keys spring*
1) "spring:session:sessions:96c83240-f939-4fd1-ac2c-93542f883aef"
2) "spring:session:sessions:56baf3c6-7a5c-483b-b04a-422b8a2be1b7"
*/
com/pcy/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/security/permitall").permitAll() // 允许所有人访问
.requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
.requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
.requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
.requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
.anyRequest().authenticated() // 其他所有请求需要认证
)
.formLogin(form -> form
// .loginPage("/custom-login") // 将登录页面的路径改为 "/custom-login" 打开会循环重定向
.defaultSuccessUrl("http://localhost:8080/swagger-ui/index.html", true) // 登录成功后的跳转路径
.permitAll()
)
.rememberMe(rememberMe -> rememberMe
.rememberMeServices(rememberMeServices()) // 配置 Remember Me 服务
)
.userDetailsService(userDetailsService) // 设置 UserDetailsService
.httpBasic(withDefaults()) // 使用 HTTP Basic 认证
.csrf(csrf -> csrf.disable()); // 禁用 CSRF
return http.build();
}
}
......
//【问答环节】
分析一下freemarker这个配置的含义
freemarker:
template-loader-path: /templates/
suffix: .ftl
/*
这些配置项的含义如下:
template-loader-path: /templates/: 这个配置指定了 Freemarker 模板文件的加载路径,也就是 /templates/ 目录。Spring Boot 会在这个目录下寻找所有的 .ftl 模板文件。
suffix: .ftl: 这个配置指定了 Freemarker 模板文件的文件后缀,也就是 .ftl。在控制器中返回视图名称时,Spring 会自动添加这个后缀来查找相应的模板文件。
配置的工作原理
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "loginPage"; // 返回的视图名
}
}
在 LoginController 中的 login() 方法中,你返回的是 "loginPage":
因为在 application.yaml 中已经配置了 template-loader-path 和 suffix,Spring Boot 会根据这些配置来查找模板文件:
它会在 template-loader-path 配置的 /templates/ 目录下寻找文件。
它会在视图名称 "loginPage" 后面自动添加 .ftl 后缀。
因此,最终 Spring Boot 会查找路径 /templates/loginPage.ftl,并使用这个模板文件来渲染登录页面。这就是为什么当你在控制器中返回 "loginPage" 时,Freemarker 能正确地找到并渲染 loginPage.ftl 模板。
如果你想修改 Freemarker 模板文件的目录或后缀,可以调整 application.yaml 中的相应配置。
视图名称在 return 中不需要包含目录或后缀,Spring 会根据配置自动处理。
*/
SpringBoot + Vue企业级狐狸
@RequestMapping 注解指定控制器类中的方法可以处理哪些格式的URL请求
@RequestMapping("/hello") 说明该方法将接收并处理格式为/hello的HTTP请求
@RestController 注解指定本类承担着SpringBoot项目的'控制器'效果
包名 | 所放置的业务代码类型 |
---|---|
common | 放置了通用的参数和业务方法 |
controller | 放置了针对各业务请求的控制类 |
domain | 放置了各种业务实体类 |
mapper | 放置了针对MyBatis框架的映射关系类 |
service | 放置了诸多实现业务逻辑的类 |